#
# peerscape.core.policy
#
# Copyright 2008 Helsinki Institute for Information Technology
# and the authors.
#
# Authors: Ken Rimey <rimey@hiit.fi>
#

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""
Authorization Scheme

Here we continue the story begun in item.py by defining the rules for
when a principal (the party controlling a private key) can sign a
particular item.

While the creator of a dataset is always authorized to sign items in
that dataset, policy declarations (policy items) can be added to the
dataset to authorize additional principals to sign items subject to
specific limitations.  The policy declaration identifies the
authorized principals by pointing at a dataset specifying a membership
list in the form of a set of membership nominations (auth items).  The
policy declaration may also restrict the authorized principals to
those for which the nomination specifies a particular role.

We intend to use such references from policy declarations to
membership lists in a fairly structured way.  Normally only two
special kinds of datasets will define membership lists: personal
profiles and group profiles (dataset types "principal" and "group",
respectively).  Policy declarations in other types of datasets
(content datasets) will generally refer to profiles.  Moreover, policy
declarations in a personal profile or a group profile will generally
refer back to that same profile, rather than to any other dataset.

(Group profiles use membership lists to define the group membership,
naturally.  Personal profiles use membership lists to define
friendships.)

A dataset nominates a principal as a member by defining a value for
the key ["auth", <principal>], where <principal> is the principal's
information ID (IID) encoded in hexadecimal.  The value must be a JSON
object, which may specify a list of named roles as the value of
"roles", and may also include other information.

Each item in a dataset must be signed, and may also be countersigned.
That is, each item has a primary signature and may optionally have a
secondary signature.  If there is both a signing principal and a
countersigning principal, it is the signing principal that must be
authorized.  The authorization rules are as follows.

- The creator of a dataset (the principal that signs the root item)
  can sign any item in that dataset.

- If the dataset defines a value for a key of the form ["policy",
  "sign", <IID>, ...], the fourth and following elements of the key
  comprise a pattern, and principals nominated in the dataset
  identified by the hexadecimal IID are authorized to sign any
  key-value item with a key matching the pattern.  That is, these
  members can add, update, and delete such keys.  The value of the
  policy declaration is interpreted as a JSON object.  If this
  includes a list of roles as the value of "roles", the authorization
  only applies to members with one of those roles.

- If the dataset defines a value for a key of the form ["policy",
  "sign", "*", ...], the meaning is similar, except that any principal
  at all is authorized to sign any key-value item with a key matching
  the pattern.  We call this a wildcard policy declaration.

- If the dataset defines a value for a key of the form ["policy",
  "sign", "!", ...], the meaning is as if the "!" were replaced by the
  hexadecimal IID of this same dataset.  This is just a convenient
  shorthand.  We call either form a local policy declaration.

- Special exceptions prohibit signing of membership nominations in
  certain cases.  A wildcard policy declaration does not permit
  signing of a membership nomination if that membership nomination
  specifies a nonempty list of roles.  A local policy declaration does
  not permit signing of a membership nomination if the roles assigned
  to the new member exceed those assigned to the signing principal,
  unless the signing principal has the role "admin".

A pattern is matched against a key element-by-element.  "*" matches
any string.  "!" matches the hexadecimal information ID of the
countersigning principal, or the signing principal if there is only
one signature.  "..." matches all the remaining elements of the key,
of which there must be at least one.  Any other string matches only an
identical string in the key.

By convention, group profiles ordinarily include a policy declaration of
the form ["policy", "sign", "!", "..."] with a value of {"roles":["admin"]}.
This ensures that group administrators have essentially the same
rights as the creator of the group.

Also, personal and group profiles generally include a policy
declaration of the form ["policy", "sign", "!", "auth", "!"] with a
value of {}.  This allows any member to nominate a new member, as long
as the new member countersigns the nomination.  It also allows members
to update or delete their own nominations.  Most importantly, it
enables the following to-whom-it-may-concern membership invitation
procedure:

1. Member A creates a new cryptographic key pair B, signs a
   membership nomination for B using A, countersigns it using B,
   and then sends the nomination and B (including the private key!)
   to the invitee.

2. The invitee C signs a membership nomination for C using B, and
   countersigns it using C.  The invitee then deletes the membership
   nomination for B, signing the tombstone using C, and countersigning
   it using B.
"""

from peerscape.lib import json
from peerscape.core.iid import hex, is_iid
from peerscape.core.path import path_to_list

class Unauthorized(Exception):
    pass

def verify(db, item, check_subscription=True):
    """
    Check that the item's signature is authorized and correct.

    An Unauthorized exception is raised if there is a problem.

    To guarantee the integrity of the database, the verification must
    be performed in the same transaction that inserts the item.
    """
    iid = hex(item.iid)
    if item.is_root():
        root = item
        if check_subscription and db.get_dataset_info(iid) is None:
            raise Unauthorized('No subscription.')
    else:
        root = db.get_root_item(iid)
        if not root:
            raise Unauthorized('No root.')

    if not item.verify_signatures(root):
        raise Unauthorized('Bad signature.')

    if item.pk is None or item.pk == root.pk:
        return                  # OK, dataset creator can sign any item.

    principal = item.get_signing_principal(root)
    principal2 = item.get_countersigning_principal(root)
    principal, principal2 = hex(principal), hex(principal2 or principal)

    for path in db.list(iid, ['policy', 'sign']):
        key = path_to_list(path)
        if len(key) < 2:
            continue            # malformed policy declaration

        group, pattern = key[0], key[1:]
        if not match(pattern, item.key, principal2):
            continue            # pattern does not match

        if group == '*':
            pass
        elif group == '!':
            group = iid
        elif is_iid(group):
            group = hex(group)  # normalize case
        else:
            continue            # malformed group specification

        if group == '*':
            member_roles = []
        else:
            member_info = db.get_json(group, ['auth', principal])
            if member_info is None:
                continue        # not a member
            member_roles = member_info.get('roles', [])
            assert isinstance(member_roles, list)

        policy_info = db.get_json(iid, ['policy', 'sign'] + key)
        if 'roles' in policy_info:
            acceptable_roles = policy_info['roles']
            assert isinstance(acceptable_roles, list)
            if not any(x in member_roles for x in acceptable_roles):
                continue        # member not authorized

        if item.is_auth() and item.value is not None:
            # The value of an auth item is always a valid JSON object.
            item_info = json.loads(item.value)
            item_roles = item_info.get('roles', [])
            assert isinstance(item_roles, list)
            if group == '*':
                if item_roles:
                    continue    # assigning roles not permitted
            elif group == iid and 'admin' not in member_roles:
                if any(x not in member_roles for x in item_roles):
                    continue    # can only pass roles along

        return                  # OK, authorized by policy declaration.

    raise Unauthorized

def match(pattern, key, principal=None):
    """
    Test if the pattern matches the key.

    >>> match(['one', 'two'], ['one', 'two'])
    True
    >>> match(['one', 'two'], ['blah', 'two'])
    False
    >>> match(['one', 'two'], ['one', 'blah'])
    False
    >>> match(['one', 'two'], ['one', 'two', 'three'])
    False
    >>> match(['one', 'two', 'three'], ['one', 'two'])
    False

    >>> match(['one', '...'], ['one', 'two', 'three'])
    True
    >>> match(['one', '...'], ['one', 'two'])
    True
    >>> match(['one', '...'], ['one'])
    False

    >>> match(['one', '*', 'three'],  ['one', 'blah', 'three'], principal='1234')
    True
    >>> match(['*', '*', 'three'],  ['one', 'two', '*'], principal='1234')
    False
    >>> match(['one', '!', 'three'],  ['one', '1234', 'three'], principal='1234')
    True
    >>> match(['one', '!', 'three'],  ['one', 'blah', 'three'], principal='1234')
    False
    """
    n = len(pattern)

    if pattern[-1] == '...':
        if len(key) < n:
            return False
        n -= 1
        pattern = pattern[:n]
        key = key[:n]
    else:
        if len(key) != n:
            return False

    for i in range(n):
        if pattern[i] == '*':
            pass
        elif pattern[i] == '!':
            assert principal
            if key[i] != principal:
                return False
        else:
            if key[i] != pattern[i]:
                return False

    return True

if __name__ == '__main__':
    import doctest
    doctest.testmod()
